package com.xbongbong.paas.gateway.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.xbongbong.i18n.util.I18nMessageUtil;
import com.xbongbong.paas.constant.PaasConstant;
import com.xbongbong.paas.constant.TimeConstant;
import com.xbongbong.paas.enums.PlatFormEnum;
import com.xbongbong.paas.enums.UrlPermissionEnum;
import com.xbongbong.paas.gateway.config.GatewayConstant;
import com.xbongbong.paas.gateway.config.GatewayOuterConstant;
import com.xbongbong.paas.gateway.manager.feign.UserFeignClient;
import com.xbongbong.paas.pojo.dto.UserInfoDTO;
import com.xbongbong.paas.pojo.vo.UserInfoVO;
import com.xbongbong.paas.redis.PaasRedisHelper;
import com.xbongbong.paas.redis.RedisPrefixConstant;
import com.xbongbong.paas.toolbox.pojo.AppPermissionPojo;
import com.xbongbong.paas.toolbox.pojo.ModulePermissionPojo;
import com.xbongbong.paas.toolbox.pojo.PermissionTermsPojo;
import com.xbongbong.paas.toolbox.util.OuterTokenUtil;
import com.xbongbong.paas.toolbox.util.StringUtil;
import com.xbongbong.paas.toolbox.wrap.UserVO;
import com.xbongbong.paas.toolbox.wrap.XbbResponse;
import com.xbongbong.pro.constant.I18nStringConstant;
import com.xbongbong.pro.enums.errorcodes.SystemErrorCodeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;

import static com.xbongbong.paas.constant.MessageConstant.NO_PERMISSION_MSG;
import static com.xbongbong.paas.gateway.filter.AuthenticationFilter.XBB_GATE_CACHE_REQUEST_BODY_JSON_KEY;

/**
 * 权限验证过滤器
 *
 * @author kaka
 * @time 2018-8-5 14:44
 */
@Component
public class AuthorizationFilter implements GlobalFilter, Ordered {

    private static  final Logger LOG = LoggerFactory.getLogger(AuthorizationFilter.class);

    static final String XBB_GATE_CACHE_USER_NAME_KEY = "xbbCachedRequestUserName";

    static final String XBB_GATE_CACHE_USER_KEY = "xbbCachedRequestUserJson";

    @Resource
    private UserFeignClient userFeignClient;
    @Resource
    private PaasRedisHelper paasRedisHelper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //request请求
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        // 判断请求的链接是否不需要校验
        for (String noFilterPrefix : GatewayConstant.NO_FILTER_PATH_PREFIX) {
            if(path.startsWith(noFilterPrefix)) {
                return chain.filter(exchange);
            }
        }
        //path对应的权限枚举
        UrlPermissionEnum urlPermissionEnum = UrlPermissionEnum.getByUrl(path);
        //请求body
        JSONObject requestBodyJson = (JSONObject)exchange.getAttributes().get(XBB_GATE_CACHE_REQUEST_BODY_JSON_KEY);
        
        // 判断请求的链接是否是外链
        for (String noFilterPrefix : GatewayOuterConstant.OUTER_NO_FILTER_PATH_PREFIX) {
            if(path.startsWith(noFilterPrefix)) {
                try {
                    String token = requestBodyJson.getString("token");
                    Boolean isExist = OuterTokenUtil.getDecryptToken(token);
                    if ( !isExist ) {
                        DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer()
                                .write(formatError(SystemErrorCodeEnum.API_ERROR_100404.getCode(), SystemErrorCodeEnum.API_ERROR_100404.getMsg()).getBytes(StandardCharsets.UTF_8));
                        HttpHeaders headers = exchange.getResponse().getHeaders();
                        // 指定错误返回的Content-Type
                        headers.add("Content-Type", PaasConstant.RESPONSE_CONTENT_TYPE);
                        return exchange.getResponse().writeWith(Mono.just(dataBuffer));
                    }
                    return chain.filter(exchange);
                } catch (Exception e) {
                    DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer()
                            .write(formatError(SystemErrorCodeEnum.API_ERROR_100404.getCode(), SystemErrorCodeEnum.API_ERROR_100404.getMsg()).getBytes(StandardCharsets.UTF_8));
                    HttpHeaders headers = exchange.getResponse().getHeaders();
                    // 指定错误返回的Content-Type
                    headers.add("Content-Type", PaasConstant.RESPONSE_CONTENT_TYPE);
                    return exchange.getResponse().writeWith(Mono.just(dataBuffer));
                }
            }
        }
        
        try {
            //校验核心参数
            checkParams(requestBodyJson);
        } catch (JSONException je) {
            DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer()
                    .write(formatError(SystemErrorCodeEnum.API_ERROR_100015.getCode(),
                    I18nMessageUtil.getMessage(I18nStringConstant.CORE_PARAMETER_FORMAT_ERROR)).getBytes(StandardCharsets.UTF_8));
            HttpHeaders headers = exchange.getResponse().getHeaders();
            // 指定错误返回的Content-Type
            headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
            return exchange.getResponse().writeWith(Mono.just(dataBuffer));
        }

        // 基本信息 corpid 和 userId
        String corpid = requestBodyJson.getString("corpid");
        String userId = requestBodyJson.getString("userId");

        // menuId 是用于表单/报表统计等更细的权限判断
        Long menuId = requestBodyJson.getLong("menuId");
        // appId 是用于表单/报表统计等更细的权限判断
        Long appId = requestBodyJson.getLong("appId");

        UserVO user = null;
        // 增加缓存，不用每次访问接口获取权限
        String loginUserKey = corpid + "____" + userId;
        String userVOStr = paasRedisHelper.getValue(RedisPrefixConstant.PAAS_LOGIN_USER_INFO, loginUserKey);
        if (!StringUtil.isEmpty(userVOStr)) {
            user = JSON.parseObject(userVOStr, UserVO.class);
        }

        if (user == null || Objects.isNull(user.getDataPermissionDetail())) {
            //获取用户和权限信息
            UserInfoDTO userInfoDTO = new UserInfoDTO();
            userInfoDTO.setCorpid(corpid);
            userInfoDTO.setUserId(userId);
            userInfoDTO.setPlatform(PlatFormEnum.INTERNAL.getValue());
            userInfoDTO.setPaasPermissionsFlag(true);
            userInfoDTO.setUserDetailFlag(true);
            userInfoDTO.setCompanyInfoFlag(false);
            Locale locale = LocaleContextHolder.getLocale();
            String lang = locale.toString();
            XbbResponse<UserInfoVO> userInfoResp = userFeignClient.getUserInfo(userInfoDTO, lang);
            UserInfoVO userInfo;
            if (userInfoResp.isSuccess()) {
                userInfo = userInfoResp.getResult();
            } else {
                DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer()
                        .write(formatError(userInfoResp.getCode(), userInfoResp.getMsg()).getBytes(StandardCharsets.UTF_8));
                HttpHeaders headers = exchange.getResponse().getHeaders();
                // 指定错误返回的Content-Type
                headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
                return exchange.getResponse().writeWith(Mono.just(dataBuffer));
            }

            user = userInfo.getUser();
            paasRedisHelper.setValue(RedisPrefixConstant.PAAS_LOGIN_USER_INFO, loginUserKey, user, TimeConstant.SEARCH_EFFECTIVE_TIME);
        }

        if(!hasPermission(urlPermissionEnum, user, appId, menuId)) {
            DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer()
                    .write(formatNoPermissionError(urlPermissionEnum.getMemo()).getBytes(StandardCharsets.UTF_8));
            HttpHeaders headers = exchange.getResponse().getHeaders();
            // 指定错误返回的Content-Type
            headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
            return exchange.getResponse().writeWith(Mono.just(dataBuffer));
        }
        HttpHeaders headers = exchange.getResponse().getHeaders();
        // 指定错误返回的Content-Type
        headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
        // userName 到 request 缓存
        exchange.getAttributes().put(XBB_GATE_CACHE_USER_NAME_KEY, user.getName());
        // user 信息到 request 缓存
        exchange.getAttributes().put(XBB_GATE_CACHE_USER_KEY, user);
        return chain.filter(exchange);
    }

    /**
     *  比AuthenticationFilter优先级低1
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1002;
    }

    /**
     * 检查请求体中的核心参数是否合法，能够正常转换成需要的类型，
     * 若不能正常转换，则抛出 JSONException
     * @param requestBodyJson 请求体
     * @throws JSONException 若出现 JSONException 则抛出
     */
    private void checkParams(JSONObject requestBodyJson) throws JSONException {
        requestBodyJson.getString("corpid");
        requestBodyJson.getString("userId");
        requestBodyJson.getString("platform");
        requestBodyJson.getLong("menuId");
        requestBodyJson.getLong("appId");
    }

    /**
     * 权限校验
     * @param urlPermission 访问的path对应的权限枚举
     * @param appId 应用id
     * @param menuId 菜单id
     * @return true 表示有权限，否则无权限
     */
    private boolean hasPermission(UrlPermissionEnum urlPermission, UserVO userVO, Long appId, Long menuId) {

        // 无需校验的链接
        if(urlPermission == null) {
            return true;
        }

        // 管理员或老板具有最高权限
        if (userVO.isAdminOrBoss()) {
            return true;
        }

        // 管理员有权限的链接（操作或页面）
        if (urlPermission.getType() == PaasConstant.URL_PERMISSION_TYPE_ADMIN) {
            if (userVO.isAdminOrBoss()) {
                return true;
            } else {
                // TODO 后期如需兼容该链接也支持赋权则需在此处再添加逻辑
                return false;
            }
        } else if (urlPermission.getType() == PaasConstant.URL_PERMISSION_TYPE_SAAS) {
            // SAAS权限
            Set<String> saasPermissions = userVO.getPermSet();
            if (saasPermissions != null && saasPermissions.contains(urlPermission.getPermission())) {
                return true;
            }
        } else if (urlPermission.getType() == PaasConstant.URL_PERMISSION_TYPE_PAAS) {
            /*
             * 权限校验逻辑
             */
            if (appId != null && menuId!= null) {
                String paasPermissions = userVO.getPaasPermissions();
                return checkPermission(urlPermission, paasPermissions, appId, menuId);
            }
        }

        return false;
    }

    /**
     * 根据用户具有的权限，检测改用户是否有该应用该表单/报表该链接的操作权限
     * @param urlPermission 链接对应的权限
     * @param paasPermissions 该用户具有的paasPermissions
     * @param appId 指定PAAS模块的某个应用
     * @param menuId 指定PAAS模块的某个菜单，可能是表单或统计
     * @return true 表示有权限，false表示无权限
     */
    private boolean checkPermission(UrlPermissionEnum urlPermission, String paasPermissions, Long appId, Long menuId) {

        List<AppPermissionPojo> appPerms;
        try {
            appPerms = JSON.parseArray(paasPermissions, AppPermissionPojo.class);
        } catch (Exception e) {
            LOG.error("checkPermission paasPermissions数据不规范", e);
            return false;
        }

        if (appPerms == null || appPerms.size() < 1) {
            return false;
        }

        for (AppPermissionPojo appPerm : appPerms) {
            if (appId.equals(appPerm.getAppId())) {
                List<ModulePermissionPojo> modulePerms = appPerm.getModules();
                if (modulePerms == null || modulePerms.size() < 1) {
                    continue;
                }

                for (ModulePermissionPojo modulePerm : modulePerms) {
                    if (menuId.equals(modulePerm.getId())) {
                        PermissionTermsPojo permTerms = modulePerm.getPermission();
                        // 利用反射机制去调用相应的getter方法获取权限
                        if (permTerms != null) {
                            String operation = urlPermission.getOperation();
                            String firstLetter = operation.substring(0, 1).toUpperCase();
                            String getter = "get" + firstLetter + operation.substring(1);
                            Method method;
                            Integer value = null;
                            try {
                                method = permTerms.getClass().getMethod(getter,null);
                                value = (Integer) method.invoke(permTerms, null);
                            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                                LOG.error("PermissionTermsPojo 调用 getter方法事出错", e);
                            }

                            if (value != null && value.equals(1)) {
                                return true;
                            }
                        }
                    }
                }
            }
        }
        return false;
    }

    /**
     * 构建无权限错误返回数据，目前格式如下
     * {
     *     "code": 100017,
     *     "msg": "你没有 %s 的权限!",
     *     "empty": true,
     *     "success": false,
     *     "totalCount": 0
     * }
     *
     * @author kaka
     * @time 2018-8-1 10:55
     */
    private String formatNoPermissionError(String permissionMemo) {
        XbbResponse verifyError = new XbbResponse(SystemErrorCodeEnum.API_ERROR_100017);
        verifyError.setMsg(String.format(I18nMessageUtil.getMessage(NO_PERMISSION_MSG), permissionMemo));
        return JSON.toJSONString(verifyError);
    }

    /**
     * 构建错误返回数据
     * @param code 错误码
     * @param msg 错误信息
     * @return 错误返回json数据
     */
    private String formatError(Integer code, String msg) {
        XbbResponse verifyError = new XbbResponse(code, msg);
        return JSON.toJSONString(verifyError);
    }
}
